/**
 * Copyright 2006 gworks.com.au
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License. (http://www.apache.org/licenses/LICENSE-2.0)
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for 
 * the specific language governing permissions and limitations under the License.
 *
 * <dhamma-message src="Atisha (11th century Tibetan Buddhist master)">
 * 	The greatest achievement is selflessness. The greatest worth is self-mastery.
 * 	The greatest quality is seeking to serve others. The greatest precept is continual awareness.
 * 	The greatest medicine is the emptiness of everything. The greatest action is not conforming with the worlds ways.
 * 	The greatest magic is transmuting the passions. The greatest generosity is non-attachment.
 * 	The greatest goodness is a peaceful mind. The greatest patience is humility.
 * 	The greatest effort is not concerned with results. The greatest meditation is a mind that lets go.
 * 	The greatest wisdom is seeing through appearances. 
 * </dhamma-message>
 */
package org.javaongems.server.rpc.io;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.log4j.Logger;
import org.javaongems.client.service.HttpPostRpc;
import org.javaongems.runtime.fileupload.MonitoredDiskFileItemFactory;
import org.javaongems.runtime.fileupload.MonitoredOutputStreamListener;
import org.javaongems.runtime.lang.StringUtils;
import org.javaongems.server.GemServe;

/**
 * <p>Provides a basic post form data keeper action</p>
 * <p>Responsibilities:
 * <ul>
 * 	<li> support a configurable timeout period when waiting for the associated 
 * 		service to complete
 * 	<li> support a configurable threshold so that the file upload data can use
 * 		file persistence when post's threshold is exceeded
 * 	<li> support a configurable repository location where is a file is persisted
 * 		it can temporarily stored that location
 * 	<li> support a configurable max upload file size to ensure that falks dont 
 * 		attempt to add unacceptably large content 
 * 	<li> acquire the payload from the request when a non-multi-part enctype
 * 		was specified as part of the http post
 * 	<li> acquire the payload from the request when a multi-part enctype
 * 		was specified as part of the http post (special boundary parsing)
 * 	<li> add payload to http session
 * 	<li> block until the http post pick up service has completed
 * </ul>
 * </p>
 * <p>Collaborators:
 * <ul>
 * 	<li> Http Post Form - client that submitted the post
 *  <li> {@link BaseHttpPostPickupRpcImpl} - base service that will pickup and complete
 *  	the operation on the posted data while the keeper waits for it to finish 
 * </ul>
 * </p>
 * <p>Constraints:
 * <ul>
 * 	<li> Uses Java wait/notify inter-thread communication on the payload object. 
 * 		This will not work in a clustered J2EE environment.
 * 	<li> This server side implementation was design in theory to support multiple
 * 		concurrent uploads from the same Gem application. However, in practice 
 * 		I have not found a browser that is able to do multi-part posts 
 * 		concurrently. See note below. 
 * </ul>
 * </p>
 * <p>Notes:
 * <ul>
 * 	<li> Both FF-1.5.x and IE6.0.x appear to be unable to send multiple mutli-part
 * 		posts concurrently. It appears like once these browsers get into a large
 * 		upload state, they are unable to send any other asynchronous signals
 * 		to the server. Interestingly all content gets uploaded, but the content is  
 * 		unable to be picked up. If a new browser process is initiated then this 
 * 		does not suffer this problem. 
 * </ul>
 * </p>
 * 
 * TODO: Abstract this implementation and let the pub/sub behavior be pluggable.   
 * @author Ashin Wimalajeewa (ash)
 */
public class HttpPostGateKeeperAction implements GemServe.Action, MonitoredOutputStreamListener {
	final static public String THRESHOLD_PARAM = "gems.io.httppost.threshold-param";
	final static public String REPOSITORY_PARAM = "gems.io.httppost.repository-param";
	final static public String MAXSIZE_PARAM = "gems.io.httppost.maxsize-param";
	final static public String KEEPER_TIMEOUT_PARAM = "gems.io.httppost.keeper-timeout-param";
	
	final static public int DEFAULT_THRESHOLD = 256 * 1024;
	final static public String DEFAULT_TMP_DIR = System.getProperty("java.io.tmpdir");
	
	final static private Logger LOGGER = Logger.getLogger(HttpPostGateKeeperAction.class);
	
	final private ThreadLocal<HttpPostPayload> threadsPayload = new ThreadLocal<HttpPostPayload>();
	final private MonitoredDiskFileItemFactory factory = new MonitoredDiskFileItemFactory(this);
		  private long maxSize = -1; 
		  private long timeOut = 1000 * 60 * 5;	// 5 min
	
	/**
	 * Reads the following values from the from the servlet context's init 
	 * parameters and configures this action appropriated:
	 * <ul>
	 * 	<li> gems.io.httppost.threshold-param
	 * 	<li> gems.io.httppost.repository-param
	 *  <li> gems.io.httppost.maxsize-param
	 *  <li> gems.io.httppost.keeper-timeout-param
	 * </ul>
	 * @see org.javaongems.server.GemServe.Action#setServletContext(javax.servlet.ServletContext)
	 */
	public void setServletConfig(ServletConfig cfg) {
		String thresholdAsStr = cfg.getInitParameter(THRESHOLD_PARAM);
		int threshold = DEFAULT_THRESHOLD;
		if (!StringUtils.isEmpty(thresholdAsStr))
			threshold = Integer.parseInt(thresholdAsStr);
		factory.setSizeThreshold(threshold);
		LOGGER.info("threshold-param=" + threshold);
		
		String repStr = cfg.getInitParameter(REPOSITORY_PARAM);
		if (StringUtils.isEmpty(repStr))
			repStr = DEFAULT_TMP_DIR;
		factory.setRepository(new File(repStr));
		LOGGER.info("repository-param=" + repStr);
		
		String maxSizeAsStr = cfg.getInitParameter(MAXSIZE_PARAM);
		if (!StringUtils.isEmpty(maxSizeAsStr))
			maxSize = Long.parseLong(maxSizeAsStr);
		LOGGER.info("maxsize-param=" + maxSize);

		String timeOutAsStr = cfg.getInitParameter(KEEPER_TIMEOUT_PARAM);
		if (!StringUtils.isEmpty(timeOutAsStr))
			timeOut = Long.parseLong(timeOutAsStr);
		LOGGER.info("keeper-timeout-param=" + timeOut);
	}
	
	/**
	 * Determines whether the post was multi-part or not and invoked to 
	 * appropriate parsing facility
	 * @see org.javaongems.server.GemServe.Action#execute(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
	 */
	public void execute(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		boolean multiPart = ServletFileUpload.isMultipartContent(req);
		HttpSession session = req.getSession();
		String[] gemIdOrderNo = HttpPostRpc.Utils.parseGemIdAndOrderNo(req.getPathInfo());
		HttpPostPayload payload = new HttpPostPayload(multiPart);

		threadsPayload.set(payload);
		initiaisePayload(req, payload);
		payload.addToSession(gemIdOrderNo[0], gemIdOrderNo[1], session);
		if (LOGGER.isDebugEnabled()) LOGGER.debug("about to process payload [" + gemIdOrderNo[0] + "," + gemIdOrderNo[1] + "]");
		try {
			if (multiPart) 
				acquireMultipartPayloadFromRequest(req, payload);
			else 
				acquireStandardPayloadFromRequest(req, payload);
			prepareAndWaitForPickup(payload, session, gemIdOrderNo);
		} finally {
			threadsPayload.set(null);
			HttpPostPayload.removeFromSession(session, gemIdOrderNo[0], gemIdOrderNo[1]);
		}
	}

	/**
	 * Initialises the payload in preparation for progress pinging
	 * @param req - the http servlet request from which the payload is sourced
	 * @param payload - the payload to store progress information in
	 */
	private void initiaisePayload(HttpServletRequest req, HttpPostPayload payload) {
		payload.progress.currentPhase = HttpPostRpc.ProgressInfo.UPLOADING_PHASE;
		payload.progress.phaseName = "uploading";
		payload.progress.totalBytes = req.getContentLength();
	}
	
	/**
	 * Basic parser that pulls all from straight from the HttpServletRequests's
	 * parameter map.
	 *  
	 * @param req - the http request
	 * @param store - the payload to store the form's data in
	 * @throws IOException
	 */
	protected void acquireStandardPayloadFromRequest(HttpServletRequest req, HttpPostPayload store) throws IOException {
		Map<String, String> data = req.getParameterMap();
		store.formData.putAll(data);
	}

	/**
	 * Apache commons fileuplaod based parser facility which sets the max size before
	 * parsing the content.
	 * 
	 * @param req - the http request
	 * @param store - the payload to store the form's data and form's file data in
	 * @throws IOException
	 */
	protected void acquireMultipartPayloadFromRequest(HttpServletRequest req, HttpPostPayload store) throws IOException {
		ArrayList<FileItem> retList = new ArrayList<FileItem>();
		ServletFileUpload fu = new ServletFileUpload(factory);
		fu.setSizeMax(maxSize);
		List files;
		try {
			if (LOGGER.isDebugEnabled()) LOGGER.debug("attempting to parse multi-part request");
			files = fu.parseRequest(req);
			if (LOGGER.isDebugEnabled()) LOGGER.debug("request parsed successfully");
		} catch (FileUploadException err) {
			throw new IOException(err.getMessage());
		}
		int max = files.size();
		for (int cntr = 0; cntr < max; cntr++) {
			FileItem curr = (FileItem) files.get(cntr);
			String name = curr.getFieldName();
			if (curr.isFormField())
				store.formData.put(name, curr.getString());
			else
				retList.add(curr);
		}
		FileItem[] retArr = new FileItem[retList.size()];
		retList.toArray(retArr);
		store.formFiles = retArr;
		if (LOGGER.isDebugEnabled()) {
			LOGGER.debug("payload.formData="  + store.formData);
			StringBuffer fileDetails = new StringBuffer();
			for (int i = 0; i < retArr.length; i++) 
				fileDetails.append(retArr[i]).append(" ");
			LOGGER.debug("payload.formFiles="  + fileDetails.toString());
		}
	}
	
	/**
	 * Adds the payload into the http session and then waits for the pickup 
	 * service to complete
	 * 
	 * @param payload - the payload to put in the http session
	 * @param session - the session to add it to
	 */
	protected void prepareAndWaitForPickup(HttpPostPayload payload, HttpSession session, String[] gemIdOrderNo) {
		if (LOGGER.isDebugEnabled()) LOGGER.debug("playload uploaded");
		payload.progress.currentPhase = HttpPostRpc.ProgressInfo.PROCESSING_PHASE;
		payload.progress.phaseName = "processing...";
		try {
			if (LOGGER.isDebugEnabled()) LOGGER.debug("waiting for order pick up...");
			waitForPickupAndHandling(gemIdOrderNo[0], gemIdOrderNo[1], payload);
			if (LOGGER.isDebugEnabled()) LOGGER.debug("order completed");
		} catch (InterruptedException err) {
			LOGGER.error("wait interupted. pickup may not be available.", err);
		}
	}

	/**
	 * Uses a basic wait/notify approach to block this keeper's thread until  
	 * the pickup service has completed.
	 * 
	 * @param gemId - the gem id sent by the client in the payload's form data
	 * @param orderNo - the order id sent by the client in the payload's form data
	 * @param payload - the payload that the pickup service needs to process
	 * @throws InterruptedException
	 */
	protected void waitForPickupAndHandling(String gemId, String orderNo, HttpPostPayload payload) throws InterruptedException {
		synchronized (payload) {
			payload.wait(timeOut);	
		}
	}

	// MonitoredOutputStreamListener implementation for progress monitoring
	public void onBytesRead(int bytesRead) {
		HttpPostPayload payload = threadsPayload.get();
		HttpPostRpc.ProgressInfo progress = payload.progress;
		progress.bytesRead += bytesRead;
		float perc = (float)((float)progress.bytesRead / (float)progress.totalBytes);
		progress.phasePercentageComplete =  Math.round(perc * 100);
//		int currChunkCount = (int)(progress.bytesRead / 5000);
//		int lastChunkCount = getChunkCount();
//		if (currChunkCount > lastChunkCount) {
//			setChunkCount(currChunkCount);
//			if (LOGGER.isDebugEnabled()) LOGGER.debug("read 5k and resting to give other threads a change");
//			try { Thread.sleep(200); } catch (InterruptedException err) {}
//		}
	}

	public void onDone() {
		threadsPayload.get().progress.statusMessage = "";
	}

	public void onStart(FileItem item) {
		threadsPayload.get().progress.statusMessage = "uploading " + item.getName();
	}
	
//	private int getChunkCount() {
//		HttpPostPayload payload = threadsPayload.get();
//		Integer chunks = (Integer) payload.cargo.get("chunk-count");
//		int ret = 0;
//		if (chunks == null)
//			setChunkCount(ret);
//		else
//			ret = chunks.intValue();
//		return ret;
//	}
//	
//	private void setChunkCount(int val) {
//		HttpPostPayload payload = threadsPayload.get();
//		Integer chunks = new Integer(val);
//		payload.cargo.put("chunk-count", chunks);
//	}
}
